Qt5.4文档翻译:Qt Quick编程入门,Getting Started Programming with Qt Quick
欢迎来到 QML 的世界,它是一门声明式用户界面语言。在这篇入门指南中,我们会使用来创建一个简单的文本编辑器程序。在读完这篇指南之后,妳应当能够使用QML 和Qt C++来开发应用程序了。
我们将要开发的一款应用程序,是一个简单的文本编辑器, 它能够载入、保存及进行一些文本编辑操作。 这篇指南由两部分组成。第一部分 ,使用声明 式语言QML 来设计应用程序的布局和行为。第二部分 ,使用Qt C++ 来实现文件的载入和保存。利用 Qt 的元对象系统 ,可将C++函数暴露为可被 QML对象类型 使用的属性。利用QML 和Qt C++,可高效地将界面逻辑与应用程序逻辑分离开。
完整 的源代码位于 examples/quick/tutorials/gettingStartedQml 目录中。如果 妳想观摩一下最终程序的样子,则, 可以直接跳到 运行文本编辑 器 小节。
本教程中的C++部分,假设读者拥有基本的对于Qt的编译过程的知识。
教程章节:
2. 实现 一个菜单栏( Menu Bar )
3. 构建 一个文本编辑器( Text Editor )
4. 美化 该文本编辑器( Text Editor )
关于QML 的信息,例如语法和特性,可参考 QML参考手册 。
我们首先来设计一个按钮。从功能上说,一个按钮包含着一个鼠标点击区域和一个文本标签。当用户按下该按钮时,按钮会作出一些动作。
在 QML 中,基本的可视化条目就是 Rectangle 类型。 Rectangle 这个 QML对象类型 ,拥有一些 QML属性 ,可用来控制它的外观和位置。
import QtQuick 2.3
Rectangle {
id: simpleButton
color: "grey"
width: 150; height: 75
Text {
id: buttonLabel
anchors.centerIn: parent
text: "button label"
}
}
第一行语句, import QtQuick 2.3 ,使得 qmlscene 工具载入 我们日后要使用的那些 QML类型 。每个QML 文件 中都必须有这么一行。注意 ,import 语句中会包含所引入的Qt 模块的版本号。
此处 的这个简单的矩形有一个唯一的标识符, simpleButton , 即为 id 属性。 Rectangle 对象 的属性是这样与值绑定的:列表中的每一项表示一个属性,首先是属性的名字,然后是一个冒号,然后是值。在这个示例代码中,颜色值 grey 被绑定到Rectangle 的 color 属性。类似 地,我们绑定了该Rectangle 的 width 和 height 。
Text 类型 是一个不可编辑的文本区域。我们将这个对象命名为 buttonLabel 。为了设置该 Text 区域的字符串内容,我们将某个值绑定到 text 属性。该文本标签被包含于 Rectangle 中,为了让它居中对齐,我们将该Text 对象的 anchors 属性赋值为其亲代对象,而其亲代对象的名字是 simpleButton 。 锚点也可以绑定到其它条目的锚点,这样,布局就会简单狠多。
我们将这个代码保存为 SimpleButton.qml ,然后运行 qmlscene ,以这个文件作为参数,就会显示一个灰色的矩形框,其中有一个文本标签。
要实现按钮的点击功能的话,需要使用 QML 的事件处理功能。 QML 的事件处理功能狠类似于 Qt 的信号和信号槽 机制 。会发射出信号,然后就会调用到与之相连接的信号槽。
Rectangle {
id: simpleButton
...
MouseArea {
id: buttonMouseArea
// Anchor all sides of the mouse area to the rectangle's anchors
anchors.fill: parent
// onClicked handles valid mouse button clicks
onClicked: console.log(buttonLabel.text + " clicked")
}
}
我们在simpleButton 中包含了一个 MouseArea 对象。 MouseArea 对象 ,表示的是一些交互区域,在该区域中,鼠标的移动会被探测到。对于我们示例中的按钮,我们将整个 MouseArea 都与它的亲代对象对齐,也就是 simpleButton 。 anchors.fill 这种语法,其意思是,访问一个名为 anchors 的属性组中的一个名为 fill 的属性。 QML使用 的是 基于锚点的布局 ,这样,各个条目可以以其它条目作为布局参考点,于是能够创建出非常健壮的布局。
MouseArea 拥有 狠多信号处理器,当鼠标在指定的 MouseArea 边界区域内移动时,就会被触发。其中一个就是 onClicked ,当它接受的鼠标按钮被按下时,就会被触发,而默认的按钮是左键。我们可以将动作绑定到 onClicked 处理器。在我们的这个示例中,当鼠标区域被点击时,会通过 console.log() 来输出文字内容。 console.log() 函数可用于调试目的及输出文字内容。
SimpleButton.qml 中的代码,会在屏幕上显示一个按钮,并且当该按钮被点击时输出文字内容。
Rectangle {
id: button
...
property color buttonColor: "lightblue"
property color onHoverColor: "gold"
property color borderColor: "white"
signal buttonClick()
onButtonClick: {
console.log(buttonLabel.text + " clicked")
}
MouseArea{
onClicked: buttonClick()
hoverEnabled: true
onEntered: parent.border.color = onHoverColor
onExited: parent.border.color = borderColor
}
// 使用条件操作符来确定按钮的颜色
color: buttonMouseArea.pressed ? Qt .darker(buttonColor, 1.5) : buttonColor
}
Button.qml 中有一个完整功能的按钮。 本文中的代码片断中省略掉了一些代码,以省略号代替,因为,它们或者是在之前已经说明过,或者是与当前讨论的代码无关。
使用 property type name 语法 来声明自定义的属性。在此处的代码中,声明了一个类型为 color 的属性 buttonColor ,并且将它的值绑定为 "lightblue" 。日后,会在一个条件操作符中用上 buttonColor ,来确定该按钮的填充颜色。注意:属性的赋值,可以使用 = 等号来进行,也可以使用 : 冒号表示的值绑定来进行。自定义的属性,使得Rectangle 作用域之外的那些代码能够访问到它的内部元素。有一些基本的 QML类型 ,例如 int 、 string 、 real ,还有一个叫做 variant 的类型。
将 onEntered 和 onExited 两个信号槽处理函数绑定到颜色上之后,每当鼠标悬停到按钮上方的时候,按钮的边框颜色会变成黄色,而当鼠标离开之后,其边框颜色会恢复。
在 Button.qml 中声明了一个 buttonClick() 信号,具体做法就是,在 signal 关键字之后跟上信号名字。对于所有的信号,都会自动创建对应的处理器函数,其名字以 on 开头。因此 , onButtonClick 就是 buttonClick 的处理器函数。然后, onButtonClick 就被赋值给某个动作。在我们的按钮示例中, onClicked 这个鼠标按钮处理器函数会简单地调用 onButtonClick ,导致输出一个文本字符串。 onButtonClick 使得外部对象 可以轻易地访问到 Button 的鼠标区域。例如,界面元素里可能会声明多个 MouseArea ,而 buttonClick 信号 可以更好地区分出多个不同的 MouseArea 信号处理器函数。
现在 ,我们知道了,怎么在QML 中处理基本的鼠标事件。 我们在一个 Rectangle 中创建了一个 Text 标签,自定义了它的属性,并且实现了对于鼠标动作做出响应的行为。在文本编辑器应用程序中,我们会频繁地使用这种在QML 对象中创建其它QML 对象的技术。
这个按钮,只有在用作一个组件来做出某个动作时,才会有用。在下一小节,我们会创建一个菜单,其中包含着多个这样的按钮。
到目前为止,我们学习了,如何在单个的QML 文件中创建对象,以及给对象赋予行为。在本小节中,我们会学习,如何导入QML 类型,以及,如何复用之前创建的组件来构建其它组件。
菜单 ,会显示某个列表中的内容,其中,每个列表项都能够做出某种动作。在 QML 中,可采用多种手段来创建菜单。首先,我们会创建一个包含着多个按钮的菜单,其中每个按钮会做出不同的动作。菜单的代码是位于 FileMenu.qml 中。
import QtQuick 2.3 // 导入 主 Qt QML模块
import "folderName" // 导入某个文件夹中的内容
import "script.js" as Script // 导入某个Javascript文件,并且命名为Script
上面的语法展示了如何使用 import 关键字。在以下情况下需要用这个关键字:需要使用 JavaScript文件 ;或者,需要使用不在同一个目录下的 QML文件 。由于 Button.qml 与 FileMenu.qml 位于同一个目录中,所以,我们不需要导入 Button.qml 文件就可以使用它。我们可以声明 Button{} 以直接创建一个 Button 对象,这与声明 Rectangle{} 类似。
在FileMenu.qml中:
Row {
anchors.centerIn: parent
spacing: parent.width / 6
Button {
id: loadButton
buttonColor: "lightgrey"
label: "Load"
}
Button {
buttonColor: "grey"
id: saveButton
label: "Save"
}
Button {
id: exitButton
label: "Exit"
buttonColor: "darkgrey"
onButtonClick: Qt .quit()
}
}
在 FileMenu.qml 中,我们声明了三个 Button 对象。它们被声明在一个 Row 类型当中,该类型是一个定位器,会将它的子代元素排列到一个竖向的行中去。 Button 本身 的声明是位于 Button.qml 中的, 它与我们上一小节中使用的是一样的。可在新创建的按钮中声明新的属性绑定,它们会覆盖掉在 Button.qml 中设置的属性。名为 exitButton 的按钮,当它被点击时,会关闭窗口并且退出程序。注意, Button.qml 中的信号处理器函数 onButtonClick 会被调用,而 exitButton 中的 onButtonClick 处理器函数也会被调用。
Row 声明位于 一个 Rectangle 中,这样,就为这一行按钮创建了一个矩形容器。额外的这个矩形,提供了一种非直接地将这一行按钮组织到某个菜单中去的手段。
编辑菜单 的声明是类似的。该菜单中有几个按钮,其文字内容分别是: Copy 、 Paste 和 Select All 。
现在,我们学会了如何导入及对之前创建的组件进行自定义,那么,我们可以将这些菜单页面组合起来创建一个菜单栏了,菜单栏中包含着用来选择菜单的按钮,并且,学习一下我们如何使用QML 来组织数据。
在我们的文本编辑器程序中,需要显示一个菜单栏。菜单栏能够在不同的菜单之间切换,并且,用户可以选择要显示哪个菜单。菜单的切换,就意味着,要对菜单进行更有效的组织,而不是仅仅将它们显示在一行中。QML使用模型和视图来组织数据并且显示结构化的数据。
QML 中有不同的 数据视图 ,用来显示 数据模型 。我们的菜单栏,会将各个菜单显示在一个列表中,并且会有一个菜单头,它显示一行菜单名字。菜单列表会声明在一个 ObjectModel 中。 ObjectModel 类型,包含的是一些本身已经可被显示的条目,例如 Rectangle 对象。其它 的模型类型,例如 ListModel 类型,需要有一个代理来显示它们的数据。
我们在 menuListModel 中声明两个可视化条目,分别是 FileMenu 和 EditMenu 。我们对这两个菜单进行自定义,然后将它们显示在一个 ListView 中。对应的QML 定义位于 MenuBar.qml 文件 中,而在 EditMenu.qml 中有一个简单的编辑菜单的定义。
ObjectModel {
id: menuListModel
FileMenu {
width: menuListView.width
height: menuBar.height
color: fileColor
}
EditMenu {
color: editColor
width: menuListView.width
height: menuBar.height
}
}
ListView 类型 会在一个代理的帮助下显示一个模型。代理 可将模型条目显示在一个 Row 对象中或者一个网格中。我们的 menuListModel 已经拥有可见的条目了,因此,我们不需要声明一个代理。
ListView {
id: menuListView
// 锚点被设置为与窗口锚点一致
anchors.fill: parent
anchors.bottom: parent.bottom
width: parent.width
height: parent.height
// 模型 中包含着数据
model: menuListModel
// 控制 着菜单切换过程中的动作
snapMode: ListView.SnapOneItem
orientation: ListView.Horizontal
boundsBehavior: Flickable.StopAtBounds
flickDeceleration: 5000
highlightFollowsCurrentItem: true
highlightMoveDuration: 240
highlightRangeMode: ListView.StrictlyEnforceRange
}
另外 , ListView 还继承自 Flickable ,这样,该列表视图就会对鼠标抓拖和其它手势做出响应了。上面例子中的最后一砣代码,设置了相关的那些 Flickable 属性,使得我们的视图具有设想中的手势效果。特别地, highlightMoveDuration 这个属性,改变了手势动画的持续时间。 highlightMoveDuration 的值越高,会使得菜单切换得越慢。
ListView 通过 一个索引( index )来维护模型中的条目,该模型中的每个可视化条目都可以通过 index 访问到,其在索引中的顺序与声明的顺序相同。改变 currentIndex ,就会改变 ListView 中的高亮条目。我们的菜单栏的头部就展示了这种效果。 这一行中有两个按钮,当它们被点击时都会改变当前菜单。 fileButton 被点击时,会将当前菜单切换成文件菜单, index 会被设置成 0 ,因为 FileMenu 在 menuListModel 中是第一个声明的。类似 地, editButton 被点击时,会将当前菜单切换成 EditMenu 。
labelList 矩形 的 z 值为 1 ,使得它显示在菜单栏的前面。 z 值较高的元素,会显示在 z 值较低的元素的前面。默认的值是。 es. The default z value is 0 .
Rectangle {
id: labelList
...
z: 1
Row {
anchors.centerIn: parent
spacing: 40
Button {
label: "File"
id: fileButton
...
onButtonClick: menuListView.currentIndex = 0
}
Button {
id: editButton
label: "Edit"
...
onButtonClick: menuListView.currentIndex = 1
}
}
}
我们刚刚创建的菜单栏,可通过扫动手势来切换不同的菜单,也可以通过点击上方的菜单名字来切换。菜单屏幕的切换显得直观且响应迅速。
如果 我们不加入一个可编辑的文本区域,那么,我们的程序就不叫文本编辑器了。 QML 的 TextEdit 类型 ,可用来声明一个多行的可编辑文本区域。 TextEdit 与 Text 类型 不同,后者不允许用户直接编辑其中的文字内容。
TextEdit {
id: textEditor
anchors.fill: parent
width: parent.width
height: parent.height
color: "midnightblue"
focus: true
wrapMode: TextEdit.Wrap
onCursorRectangleChanged: flickArea.ensureVisible(cursorRectangle)
}
这个编辑器,被设置了字体的颜色( color )属性,并且被设置了 wrapMode 属性,以支持自动换行。此 TextEdit 区域是位于一个可滑动的元素之中,当文本光标离开可视区域的范围时,会自动将文本内容滚动。 ensureVisible() 函数会检查光标矩形是否位于可视区域边界之外,并且相应地移动文本区域。 QML使用Javascript 的语法来编写它的脚本,并且,前面已经提到过, Javascript文件 可被导入以在QML 文件中使用。
function ensureVisible(r) {
if (contentX >= r.x)
contentX = r.x;
else if (contentX + width <= r.x + r.width)
contentX = r.x + r.width - width;
if (contentY >= r.y)
contentY = r.y;
else if (contentY + height <= r.y + r.height)
contentY = r.y + r.height - height;
}
现在,我们可以使用QML 来创建文本编辑器的布局了。这个文本编辑器有两个组件:我们之前创建的 菜单栏;以及现在所说的文本区域。QML允许我们复用组件,这样会使得我们的代码更简单,具体做法就是,导入组件并且在必要的时候进行自定义。我们的文本编辑器,将窗口分割成两个部分:屏幕中1/3的部分用来显示菜单栏;另外2/3的部分用来显示文本区域。菜单栏会显示在任何其它对象的前面。
Rectangle {
id: screen
width: 1000
height: 1000
// 屏幕 被划分成 MenuBar 和 TextArea 。
// 屏幕 上1/3的区域被分配给 MenuBar
property int partition: height / 3
MenuBar {
id: menuBar
height: partition
width: parent.width
z: 1
}
TextArea {
id: textArea
anchors.bottom: parent.bottom
y: partition
color: "white"
width: parent.width
height: partition * 2
}
}
通过导入 可复用的组件,使得我们的 TextEditor 代码看起来简单得多。然后,我们可以对主程序进行自定义,并且不需要担心那些已经拥有确定行为的属性。通过这种手段,可以轻松地创建程序布局和用户界面组件。
我们的文本编辑器看起来狠简单,所以需要做一下美化。使用QML,可以声明一些过渡效果,让我们的文本编辑器拥有动画效果。我们的菜单栏占用了屏幕上1/3的区域,因此,有必要让它只在需要用到的时候出现。
我们可以添加一个抽屉界面,它会在被点击时收起或展开菜单栏。具体地,在我们的实现当中,我们创建了一个小小的矩形,它会被鼠标点击事件做出响应。抽屉 drawer ,和该程序,有两个状态:“抽屉已打开”状态和“抽屉已关闭”状态。 drawer 元素 是一个狭长矮小的矩形。其中嵌套了一个 Image 对象,它包含了一个箭头图标,并且在抽屉中居中。每当用户点击它的鼠标区域时,抽屉就会通过 screen 标识符来向整个程序设置一个状态。
Rectangle {
id: drawer
height: 15
Image {
id: arrowIcon
source: "images/arrow.png"
anchors.horizontalCenter: parent.horizontalCenter
}
MouseArea {
id: drawerMouseArea
anchors.fill: parent
onClicked: {
if (screen.state == "DRAWER_CLOSED")
screen.state = "DRAWER_OPEN"
else if (screen.state == "DRAWER_OPEN")
screen.state = "DRAWER_CLOSED"
}
...
}
}
状态 ,其实就是一组配置选项的集合,它是使用 State 类型来声明的。可将一个状态列表绑定到 states 属性。 在我们的程序中,这两个状态就是 DRAWER_CLOSED 和 DRAWER_OPEN 。元素 的配置选项是使用 PropertyChanges 对象来声明的。在 DRAWER_OPEN 状态中,有4个元素会接收到属性变更。第一个目标, menuBar , 会将 y 属性改变成 0 。类似 地,当状态为 DRAWER_OPEN 的时候, textArea 会向下移动到一个新的位置。 textArea 、 drawer 以及抽屉的图标都会执行属性的变更,以迎合当前的状态。
states:[
State {
name: "DRAWER_OPEN"
PropertyChanges { target: menuBar; y: 0 }
PropertyChanges { target: textArea; y: partition + drawer.height }
PropertyChanges { target: drawer; y: partition }
PropertyChanges { target: arrowIcon; rotation: 180 }
},
State {
name: "DRAWER_CLOSED"
PropertyChanges { target: menuBar; y: -height; }
PropertyChanges { target: textArea; y: drawer.height; height: screen.height - drawer.height }
PropertyChanges { target: drawer; y: 0 }
PropertyChanges { target: arrowIcon; rotation: 0 }
}
]
状态变更 是狠突然的,因此需要有平滑的迁移过程。状态之间的迁移是使用 Transition 类型来定义的,然后,它们可以被绑定到对应元素的 transitions 属性中。 当状态变更成 DRAWER_OPEN 或 DRAWER_CLOSED 的时候,我们的文本编辑器会发生一次状态迁移。有一点狠重要,迁移对象需要指定一个 from 和一个 to 状态,但是,对于我们此处使用的迁移,我们可以使用通配符 * ,来表示,此迁移会应用到所有的状态变更。
在迁移过程中,可以将动画赋予给属性变更过程。我们的 menuBar 会将位置从 y: 0 变更为 y: -partition ,而我们可以使用 NumberAnimation 类型来让这个迁移过程以动画来表现。 我们声明,目标的属性会在特定的持续时间内以动画形式发生变更,并且,会使用特定的缓和曲线。缓和曲线控制着动画的速率,以及状态迁移过程中的插值行为。我们选择的缓和曲线是 Easing.OutExpo ,它会在动画的结尾降低运动速度。欲知更多信息,则参考 QML 的 动画 文档 。
transitions: [
Transition {
to: "*"
NumberAnimation { target: textArea; properties: "y, height"; duration: 100; easing.type:Easing.OutExpo }
NumberAnimation { target: menuBar; properties: "y"; duration: 100; easing.type: Easing.OutExpo }
NumberAnimation { target: drawer; properties: "y"; duration: 100; easing.type: Easing.OutExpo }
}
]
另外 一种以动画形式展现属性变更的手段就是,声明一个 Behavior 类型。迁移 只在状态变更过程中才能起作用,而 Behavior 呢,可为通用的属性变更设置动画过程。 在我们这里的文本编辑器中,箭头对象会在 rotation 属性发生变更时使用一个 NumberAnimation 来以动画形式展现该变更。
在 TextEditor.qml 中:
Behavior {
NumberAnimation { property: "rotation"; easing.type: Easing.OutExpo }
}
现在 ,学习了状态和动画的知识,我们再回过头去看那些组件的代码,我们可以改善那些组件的外观。在 Button.qml 中,我们可以在按钮被点击时添加 color 和 scale 属性的变更。颜色类型是使用 ColorAnimation 来处理动画的,而数值是使用 NumberAnimation 来处理动画的。 下 文 所展示的 on propertyName 语法,在针对单个属性做动画处理时,狠有用。
在 Button.qml 中:
...
color: buttonMouseArea.pressed ? Qt .darker(buttonColor, 1.5) : buttonColor
Behavior on color { ColorAnimation{ duration: 55 } }
scale: buttonMouseArea.pressed ? 1.1 : 1.0
Behavior on scale { NumberAnimation{ duration: 55 } }
另外 ,我们可以向QML 组件添加颜色效果(例如渐变)和透明效果来改善它们的外观。声明 Gradient 对象 的话,会覆盖掉 color 属性。妳可以使用 GradientStop 类型来在渐变中声明一个颜色。渐变是使用一个范围值来定位的,其取值范围是 0.0 到 1.0 。
在 MenuBar.qml 中:
gradient: Gradient {
GradientStop { position: 0.0; color: "#8C8F8C" }
GradientStop { position: 0.17; color: "#6A6D6A" }
GradientStop { position: 0.98; color: "#3F3F3F" }
GradientStop { position: 1.0; color: "#0e1B20" }
}
菜单 栏利用了渐变来显示一个以渐变来模拟的深度效果。第一个颜色位于 0.0 ,最后一个颜色位于 1.0 。
我们已经实现了一个狠简单的文本编辑器的界面。然后呢,用户界面已经完工了,那么,我们可以使用常规的Qt 和C++来实现程序的逻辑了。QML用来做原型工具是狠好的,可以将程序逻辑和用户界面的设计分离开来。
既然 我们已经设计好了文本编辑器的布局,那么,我们可以开始使用C++来实现该文本编辑器的功能了。将QML 与C++配套使用的话,使得我们可以使用Qt 来实现程序逻辑。 我们可以在C++程序中使用 Qt的那些Quick类 创建一个QML 上下文,并且使用 QQuickView 来显示那些QML 类型的对象。或者,我们可以将 C++代码导出 到一个扩展插件中,然后,让它作为一个新的 已标识的模块 ,以让QML 可以访问到。在使用 qmlscene 启动QML 文件的时候,只需要确保该模块位于QML 引擎用来寻找模块的其中某一个 导入路径 中就可以了。对于我们的程序,应当使用第二种实现方式。这样的话,我们就可以直接使用 qmlscene 载入该 QML 文件,而不用运行一个可执行程序。
我们会使用Qt 和C++来实现文件载入和保存功能。在QML 中注册C++类和函数,即可利用它们。它们还需要被编译为一个Qt插件,并且暴露为一个QML 模块。
对于我们的应用程序,我们需要创建以下条目:
1. Directory 类,处理 与目录相关的操作
2. File 类,它本身是一个 QObject ,用来模拟某个目录中的文件列表
3.一个插件类,用来将以上那些类注册到QML 上下文中
4. 一个Qt项目文件,用来编译该插件
5. 一个 用于定义模块的qmldir文件 ,它定义了那些要通过QML 模块暴露出来的标识符(用于导入的 URI)及内容(在这个例子中,就是我们的插件)
注意: 自从Qt 5.1 开始 , Qt Quick Dialogs 模块提供 了一个文件对话框组件,可用来从本地文件系统中选择文件。在本教程中,出于演示的目的,我们自己写了个文件对话框。
要想构造一个插件,我们需要在Qt 项目文件中设置以下属性。首先,要把必要的源代码、头文件和 Qt模块添加 到我们的项目文件中。所有的 C++代码 和项目文件都位于 filedialog 目录中。
在 filedialog.pro 中:
TEMPLATE = lib
CONFIG += qt plugin
QT += qml
DESTDIR += ../imports/FileDialog
OBJECTS_DIR = tmp
MOC_DIR = tmp
TARGET = filedialogplugin
HEADERS += \
directory.h \
file.h \
dialogPlugin.h
SOURCES += \
directory.cpp \
file.cpp \
dialogPlugin.cpp
特别地,我们将该项目与 qml 模块链接,并且使用 lib 模板来将它配置为一个插件( plugin )。我们应当将编译出来的插件放置到亲代目录的 imports/FileDialog 目录中。
在 dialogPlugin.h 中:
#include <QtQml/QQmlExtensionPlugin>
class DialogPlugin : public QQmlExtensionPlugin
{
Q_OBJECT
Q_PLUGIN_METADATA(IID "org.qt-project.QmlExtensionPlugin.FileDialog")
public:
// registerTypes 是继承自 QQmlExtensionPlugin
void registerTypes(const char *uri);
};
我们需要使用 Q_PLUGIN_METADATA 宏来导出该插件。注意,在 dialogPlugin.h 文件中,在类的顶部使用了 Q_OBJECT 宏。另外,我们需要针对该项目文件运行 qmake ,以生成必要的元对象代码。
我们的插件类 DialogPlugin ,是 QQmlExtensionPlugin 的一个子类。我们需要实现继承的函数 registerTypes() 。
在 DialogPlugin.cpp 中:
#include "dialogPlugin.h"
#include "directory.h"
#include "file.h"
#include <QtQml>
void DialogPlugin::registerTypes(const char *uri)
{
// 将Directory 类注册到QML 中,类型为"Directory",版本号为 1.0
// @uri FileDialog
qmlRegisterType<Directory>(uri, 1, 0, "Directory");
qmlRegisterType<File>(uri, 1, 0, "File");
}
registerTypes() 函数 会将我们的File 和Directory 类注册到QML 中。这个函数需要以下参数:以类名作为模板参数;主版本号;次版本号;以及我们的类在QML中的名字。 // @uri <module identifier> 这个注释,使得 Qt Creator 在编辑那些导入了此模块的QML 文件时,知道所注册的类型的存在性。
我们可以使用C++和 Qt的元对象系统 来创建QML 类型和属性。我们可以使用信号槽和信号来实现属性,使用 Qt得知 这些属性。然后,这些属性就可以在QML 中使用了。
对于文本编辑 器,我们需要能够载入及保存文件。一般地,这些功能是由一个文件对话框提供的。幸运的是,我们可以使用 QDir 、 QFile 和 QTextStream 来实现目录的读取和输入/输出流。
class Directory : public QObject {
Q_OBJECT
Q_PROPERTY (int filesCount READ filesCount CONSTANT)
Q_PROPERTY ( QString filename READ filename WRITE setFilename NOTIFY filenameChanged)
Q_PROPERTY ( QString fileContent READ fileContent WRITE setFileContent NOTIFY fileContentChanged)
Q_PROPERTY ( QQmlListProperty <File> files READ files CONSTANT)
...
Directory 类使用 Qt 的元对象系统来注册那些在完成文件处理功能时所需要的属性。 Directory 类,被导出为一个插件,并且,可在QML 中以 Directory 类型的形式使用。每一个使用 Q_PROPERTY ()宏列出的属性,都是一个QML 属性。
Q_PROPERTY 会向Qt的元对象系统声明一个属性,以及对应的读取和写入函数。例如, filename 属性 ,类型为 QString ,可使用 filename() 函数来读取,使用 setFilename() 函数来写入。另外, 还有一个与filename 属性关联的信号,名为 filenameChanged() ,当该属性发生变化时会被发射。读取和写入函数是在头文件中声明为 public 访问类型的。
类似 地,我们声明了其它属性。 filesCount 属性表示 的是某个目录中的文件个数。 filename属性 会反映出当前选中的文件的名字,而载入/保存的文件内容,是储存在 fileContent 属性中的。
Q_PROPERTY ( QQmlListProperty <File> files READ files CONSTANT)
files 这个列出属性,是某个目录中被过滤出来的所有文件的列表。 Directory 类的实现中,会过滤掉无效的文本文件;只有那些名字以 .txt 结尾的文件是有效的。另外, QList 可在QML 文件中使用,只需在C++中将它们声明为 QQmlListProperty 即可。模板 中的类需要继承自 QObject ,因此, File 类必须继承 QObject 。在 Directory 类中,由 File 对象组成的那个列表是储存在一个名为 m_fileList 的 QList 中。
class File : public QObject {
Q_OBJECT
Q_PROPERTY( QString name READ name WRITE setName NOTIFY nameChanged)
...
};
然后 ,这些属性就可以在QML 中作为 Directory 对象属性的一部分来使用。注意,我们不需要在C++代码中创建一个标识 id 属性。
Directory {
id: directory
filesCount
filename
fileContent
files
files[0].name
}
由于QML使用Javascript 的语法和结构,因此,我们可以在文件列表中遍历并且获取它们的属性。要获取第一个文件的名字属性,我们可以调用 files[0].name 。
常规 的 C++函数 也可在QML 中访问到。文件的载入和保存函数,是使用 C++实现 的,并且是使用 Q_INVOKABLE 宏来声明的。或者,我们也可以将这些函数声明为槽( slot ),那样的话也可以在QML 中访问到它们。
在 directory.h 中:
Q_INVOKABLE void saveFile();
Q_INVOKABLE void loadFile();
Directory 类还需要在目录的内容发生变化时告知其它对象。这个特性是利用信号( signal )来实现的。之前已经说过,对于 QML信号 ,会有一个对应的处理器函数,其名字为信号名字前面加上 on 。该信号名为 directoryChanged ,每当发生目录刷新时,就会被发射。刷新动作中,只是简单地重新载入目录的内容,并且更新目录中有效文件的列表。然后,那些 QML元素 ,只需要向 onDirectoryChanged 这个信号处理器附加一个动作,即可获知目录内容的变化。
列表 (list) 属性需要 多讲一讲。因为,列表属性是使用回调函数来访问及修改列表中的内容的。列表属性的类型是 QQmlListProperty<File> 。每当列表被访问时,访问函数就需要返回一个 QQmlListProperty<File> 。其中的模板类, File ,需要继承自 QObject 。另外,为了创建该 QQmlListProperty ,访问 器和修改器需要以函数指针的形式传入构造函数。此处 的列表,在我们的示例中,是一个 QList ,也需要声明成由 File 指针组成的列表。
QQmlListProperty 的构造函数是如下声明的:
QQmlListProperty ( QObject *object, void *data, AppendFunction append,
CountFunction count = 0, AtFunction at = 0, ClearFunction clear = 0);
它需要一系列的函数指针,这些函数分别做以下事情:向列表中追加元素;返回列表中元素个数;使用下标来获取指定的元素;清空列表。只有 append 函数 是必需的。注意,各个函数指针 都必须匹配 AppendFunction 、 CountFunction 、 AtFunction 或 ClearFunction 的定义。
Directory 类像这样构造一个 QQmlListProperty 实例:
QQmlListProperty <File>(this, &m_fileList, &appendFiles, &filesSize, &fileAt, &clearFilesPtr);
其中的参数分别是指向以下函数的指针:
void appendFiles( QQmlListProperty <File> *property, File *file);
File* fileAt( QQmlListProperty <File> *property, int index);
int filesSize( QQmlListProperty <File> *property);
void clearFilesPtr( QQmlListProperty <File> *property);
为了简化我们的文件对话框, Directory 类将无效的文本文件过滤掉了,无效 的文件即是那些不带 .txt 后缀名的文件。如果某个文件的文件名不带 .txt 后缀,则,在我们的文件对话框中就看不到它。并且 ,代码里会确保,保存出去的文件,其文件名必定带有一个 .txt 后缀。 Directory 使用 QTextStream 来读取及输出文件内容。
利用 Directory 对象 ,我们可以做到:获取 到文件列表,以得知该应用程序的目录中有多少个文本文件;获取选中的文件的名字,以及其内容;当目录的内容发生变化时得到通知。
要构建该插件,则针对 filedialog.pro 项目文件运行 qmake 命令,然后运行 make 命令 来构建它并且将该插件传送到 plugins 目录中。
qmlscene 工具能够导入那些 与应用程序处于同一个目录的文件。我们还可以创建一个 qmldir 文件,其中包含着我们想要导入的内容所在的位置。在本示例中,只有该插件需要导入,但是,我们也可以在 qmldir 中定义其它资源(QML类型、JavaScript文件)。
qmldir 文件 的内容:
module FileDialog
plugin filedialogplugin
我们刚刚创建的模块名为 FileDialog ,它提供了一个名为 filedialogplugin 的插件,该名字即与项目文件中的 TARGET 字段相匹配。因为 我们没有指定该插件的路径,所以, QML预期 会在与 qmldir 文件相同的目录下找到该插件。
现在,可以在QML 中导入由我们的插件注册的那些QML 类型:
import FileDialog 1.0
Directory {
id: directory
}
...
我们的 FileMenu 需要显示 出 FileDialog 对象,其中包含 着某个目录下的文本文件列表,使得用户可以通过点击列表来选择特定文件。 我们还需要将保存、载入和新建按钮与对应的动作关联起来。 FileMenu 中还包含着一个可编辑的文本输入区域,允许用户使用键盘 来输入一个文件名。
Directory 对象 是在 FileMenu.qml 文件中使用的,它向 FileDialog 对象告知,目录对象已经刷新了它的内容。 这个通知是利用信号处理器,即 onDirectoryChanged 来实现的。
在 FileMenu.qml 中:
Directory {
id: directory
filename: textInput.text
onDirectoryChanged: fileDialog.notifyRefresh()
}
为了让我们的程序尽可能地简单,我们会让文件对话框保持一直可见,并且不会显示出无效的文本文件,也就是说其文件名不带 .txt 后缀名的文件。
在 FileDialog.qml 中:
signal notifyRefresh()
onNotifyRefresh: dirView.model = directory.files
FileDialog 对象 会读取某个目录的名为 files 的列表属性,以显示其内容。那些文件 会作为某个 GridView 对象的模型,该对象会根据某个代理的数据来以一个网格显示数据条目。 该代理会管理该模型的外观,而我们的文件对话框会简单的创建一个网格,其中,文字会显示在每个条目的中间。 点击文件名,就会导致出现 一个矩形框,将该文件名高亮显示。每当 notifyRefresh 信号 被发射时, FileDialog 就会得到通知,于是重新载入目录中的文件列表。
在 FileMenu.qml 中:
Button {
id: newButton
label: "New"
onButtonClick: {
textArea.textContent = ""
}
}
Button {
id: loadButton
label: "Load"
onButtonClick: {
directory.filename = textInput.text
directory.loadFile()
textArea.textContent = directory.fileContent
}
}
Button {
id: saveButton
label: "Save"
onButtonClick: {
directory.fileContent = textArea.textContent
directory.filename = textInput.text
directory.saveFile()
}
}
Button {
id: exitButton
label: "Exit"
onButtonClick: {
Qt .quit()
}
}
现在 ,可以将我们的 FileMenu 连接到相应的动作了。 saveButton 按钮, 被点击之后,会将 TextEdit 中的文字内容复制到目录的 fileContent 属性中,然后,从可编辑的文字输入框中复制其文件名。最后,该按钮会调用 saveFile() 函数来保存文件。 loadButton 也会触发类似的执行过程。然后呢, New 按钮对应 的动作,会清空 TextEdit 的内容。
还有, EditMenu 中的那些按钮,会连接到 TextEdit 的对应功能:复制、粘贴及选中文本编辑器中的所有文字。
现在,这个应用程序已经成为一个简单的文本编辑器了,它能够接受文字内容输入,并且将内容保存到文件中。它还可以载入一个文件,并且进行文字操作。
在运行这个文本编辑器之前,需要构建出那个文件对话框的 C++插件 。 要构建它,则,进入 filedialog 目录,然后,运行 qmake ,最后使用 make 来编译它。
使用 qmlscene 命令 来运行该文本编辑器,并且传入imports 目录作为其中一个参数,这样, QML引擎 就知道该到哪里去寻找那个导入了我们的文件对话框插件的模块:
qmlscene -I ./imports texteditor.qml
完整 的源代码,位于 examples/quick/tutorials/gettingStartedQml 目录中。
http://img4.duitang.com/uploads/item/201407/18/20140718230344_3xyye.jpeg
把良心拿出来晒一晒。
Your opinionsHxLauncher: Launch Android applications by voice commands